namespace egret.socketio {

    export type Primitive = string | number | boolean;

    let re = {
        starts_with_slashes: /^\/+/,
        ends_with_slashes: /\/+$/,
        pluses: /\+/g,
        query_separator: /[&;]/,
        uri_parser: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*)(?::([^:@]*))?)?@)?(\[[0-9a-fA-F:.]+\]|[^:\/?#]*)(?::(\d+|(?=:)))?(:)?)((((?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
    };

    interface UrlParam {
        [index: number]: Primitive;
    }

    interface UriParts {
        [index: string]: Primitive;
        source?: string;
        query?: string;
        isColonUri?: boolean;
        protocol?: string;
        userInfo?: string;
        host?: string;
        port?: number;
        path?: string;
        anchor?: string;
    }

    function decode(s?: string): string {
        if (s) {
            s = s.toString().replace(re.pluses, '%20');
            s = decodeURIComponent(s);
        }
        return s;
    }

    function parseUri(str: string): UriParts {
        let parser = re.uri_parser;
        let parserKeys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "isColonUri", "relative", "path", "directory", "file", "query", "anchor"];
        let m = parser.exec(str || '');
        let parts = {};

        parserKeys.forEach((value: string, index: number) => {
            parts[value] = m[index] || '';
        });

        return parts;
    }

    function parseQuery(str?: string): Array<UrlParam> {
        let i: number, ps: Array<string>, p: string, n: number, k, v, l;
        let pairs = [];

        if (typeof (str) === 'undefined' || str === null || str === '') {
            return pairs;
        }

        if (str.indexOf('?') === 0) {
            str = str.substring(1);
        }

        ps = str.toString().split(re.query_separator);

        for (i = 0, l = ps.length; i < l; i++) {
            p = ps[i];
            n = p.indexOf('=');

            if (n !== 0) {
                k = decode(p.substring(0, n));
                v = decode(p.substring(n + 1));
                pairs.push(n === -1 ? [p, null] : [k, v]);
            }
        }

        return pairs;
    }

    export class Uri {

        private uriParts: UriParts;
        private queryPairs: Array<UrlParam>;
        private hasAuthorityPrefixUserPref: boolean;

        /**
         * Creates a new Uri object
         * @constructor
         * @param {string} str
         */
        constructor(str?: string) {
            this.uriParts = parseUri(str);
            this.queryPairs = parseQuery(this.uriParts.query);
            this.hasAuthorityPrefixUserPref = null;
        }

        /**
         * if there is no protocol, the leading // can be enabled or disabled
         * @param  {Boolean}  val
         * @return {Boolean}
         */
        hasAuthorityPrefix(val?: boolean): boolean {
            if (typeof val !== 'undefined') {
                this.hasAuthorityPrefixUserPref = val;
            }
            if (this.hasAuthorityPrefixUserPref === null) {
                return (this.uriParts.source.indexOf('//') !== -1);
            } else {
                return this.hasAuthorityPrefixUserPref;
            }
        }

        isColonUri(val?: boolean): boolean {
            if (typeof val !== 'undefined') {
                this.uriParts.isColonUri = !!val;
            } else {
                return !!this.uriParts.isColonUri;
            }
        }

        /**
        * Serializes the internal state of the query pairs
        * @param  {string} [val]   set a new query string
        * @return {string}         query string
        */
        query(val?: string): string {
            let s = '', i: number, param, l: number;
            if (typeof val !== 'undefined') {
                this.queryPairs = parseQuery(val);
            }

            for (i = 0, l = this.queryPairs.length; i < l; i++) {
                param = this.queryPairs[i];
                if (s.length > 0) {
                    s += '&';
                }

                if (param[0] === null) {
                    s += param[0];
                } else {
                    s += param[0];
                    s += '=';
                    if (typeof param[1] != 'undefined') {
                        s += encodeURIComponent(param[1]);
                    }
                }
            }

            return s.length > 0 ? '?' + s : s;
        }

        /**
         * returns the first query param value found for the key
         * @param  {string} key query key
         * @return {string}     first value found for key
         */
        getQueryParamValue(key: string): string {
            let i, param, l;

            for (i = 0, l = this.queryPairs.length; i < l; i++) {
                param = this.queryPairs[i];
                if (key == param[0]) {
                    return param[1]
                }
            }
        }

        /**
         * returns an array of query param values for the key
         * @param  {string} key query key
         * @return {array}      array of values
         */
        getQueryParamValues(key: string): Primitive[] {
            let arr = [], i, param: UrlParam, l;

            for (i = 0, l = this.queryPairs.length; i < l; i++) {
                param = this.queryPairs[i];
                if (key == param[0]) {
                    arr.push(param[1]);
                }
            }

            return arr;
        }

        /**
         * removes query parameters
         * @param  {string} key     remove values for key
         * @param  {val}    [val]   remove a specific value, otherwise removes all
         * @return {Uri}            returns self for fluent chaining
         */
        deleteQueryParam(key: string, val?: string): Uri {
            let arr = [], i, param, keyMatchesFilter, valMatchesFilter, l;

            for (i = 0, l = this.queryPairs.length; i < l; i++) {

                param = this.queryPairs[i];
                keyMatchesFilter = decode(param[0]) === decode(key);
                valMatchesFilter = param[1] === val;

                if ((arguments.length === 1 && !keyMatchesFilter) || (arguments.length === 2 && (!keyMatchesFilter || !valMatchesFilter))) {
                    arr.push(param);
                }
            }

            this.queryPairs = arr;

            return this;
        }

        /**
        * adds a query parameter
        * @param  {string}  key        add values for key
        * @param  {string}  val        value to add
        * @param  {integer} [index]    specific index to add the value at
        * @return {Uri}                returns self for fluent chaining
        */
        addQueryParam(key: string, val: Primitive, index?: number): Uri {
            if (arguments.length === 3 && index !== -1) {
                index = Math.min(index, this.queryPairs.length);
                this.queryPairs.splice(index, 0, [key, val]);
            } else if (arguments.length > 0) {
                this.queryPairs.push([key, val]);
            }
            return this;
        }

        /**
         * test for the existence of a query parameter
         * @param  {string}  key        check values for key
         * @return {Boolean}            true if key exists, otherwise false
         */
        hasQueryParam(key: string): boolean {
            let i, len = this.queryPairs.length;
            for (i = 0; i < len; i++) {
                if (this.queryPairs[i][0] == key)
                    return true;
            }
            return false;
        }

        /**
         * replaces query param values
         * @param  {string} key         key to replace value for
         * @param  {string} newVal      new value
         * @param  {string} [oldVal]    replace only one specific value (otherwise replaces all)
         * @return {Uri}                returns self for fluent chaining
         */
        replaceQueryParam(key: string, newVal: Primitive, oldVal?: Primitive): Uri {
            let index = -1, len = this.queryPairs.length, i, param;

            if (arguments.length === 3) {
                for (i = 0; i < len; i++) {
                    param = this.queryPairs[i];
                    if (decode(param[0].toString()) === decode(key) && decodeURIComponent(param[1]) === decode(oldVal.toString())) {
                        index = i;
                        break;
                    }
                }
                if (index >= 0) {
                    this.deleteQueryParam(key, decode(oldVal.toString())).addQueryParam(key, newVal, index);
                }
            } else {
                for (i = 0; i < len; i++) {
                    param = this.queryPairs[i];
                    if (decode(param[0].toString()) === decode(key)) {
                        index = i;
                        break;
                    }
                }
                this.deleteQueryParam(key);
                this.addQueryParam(key, newVal, index);
            }
            return this;
        }

        /**
         * Scheme name, colon and doubleslash, as required
         * @return {string} http:// or possibly just //
         */
        scheme(): string {
            let s = '';
            if (this.protocol) {
                s += this.protocol;
                if (this.protocol.indexOf(':') !== this.protocol.length - 1) {
                    s += ':';
                }
                s += '//';
            } else {
                if (this.hasAuthorityPrefix() && this.host) {
                    s += '//';
                }
            }
            return s;
        }

        /**
         * Same as Mozilla nsIURI.prePath
         * @return {string} scheme://user:password@host:port
         * @see  https://developer.mozilla.org/en/nsIURI
         */
        origin(): string {
            let s = this.scheme();

            if (this.userInfo && this.host) {
                s += this.userInfo;
                if (this.userInfo.indexOf('@') !== this.userInfo.length - 1) {
                    s += '@';
                }
            }

            if (this.host) {
                s += this.host;
                if (this.port || (this.path.substr(0, 1).match(/[0-9]/))) {
                    s += ':' + this.port
                }
            }

            return s;
        }

        /**
         * Adds a trailing slash to the path
         */
        addTrailingSlash(): Uri {
            let path = this.path || '';

            if (path.substr(-1) !== '/') {
                this.path = path + '/';
            }
            return this;
        }

        /**
         * Serializes the internal state of the Uri object
         * @return {string}
         */
        toString(): string {
            let path, s = this.origin();

            if (this.isColonUri()) {
                if (this.path) {
                    s += ':' + this.path;
                }
            } else if (this.path) {
                path = this.path;
                if (!(re.ends_with_slashes.test(s) || re.starts_with_slashes.test(path))) {
                    s += '/';
                } else {
                    if (s) {
                        s.replace(re.ends_with_slashes, '/');
                    }
                    path = path.replace(re.starts_with_slashes, '/');
                }
                s += path;
            } else {
                if (this.host && (this.query.toString() || this.anchor)) {
                    s += '/';
                }
            }
            if (this.query().toString()) {
                s += this.query().toString();
            }
            if (this.anchor) {
                if (this.anchor.indexOf('#') !== 0) {
                    s += '#';
                }
                s += this.anchor;
            }

            return s;
        }

        /**
         * Clone a Uri object
         * @return {Uri} duplicate copy of the Uri
         */
        clone(): Uri {
            return new Uri(this.toString());
        }

        public set protocol(v: string) {
            this.uriParts.protocol = v;
        }

        public get protocol(): string {
            return this.uriParts.protocol;
        }

        public set userInfo(v: string) {
            this.uriParts.userInfo = v;
        }

        public get userInfo(): string {
            return this.uriParts.userInfo;
        }

        public set host(v: string) {
            this.uriParts.host = v;
        }

        public get host(): string {
            return this.uriParts.host
        }

        public set port(v: number) {
            this.uriParts.port = v;
        }

        public get port(): number {
            return this.uriParts.port;
        }

        public set path(v: string) {
            this.uriParts.path = v;
        }

        public get path(): string {
            return this.uriParts.path;
        }

        public set anchor(v: string) {
            this.uriParts.anchor = v;
        }

        public get anchor(): string {
            return this.uriParts.anchor;
        }
    }

}